package com.github.uiautomator;

import android.graphics.Point;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import androidx.annotation.RequiresApi;
import android.util.Log;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.MotionEvent;

import com.github.uiautomator.compat.InputManagerWrapper;
import com.github.uiautomator.compat.WindowManagerWrapper;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Scanner;

@RequiresApi(api = Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public class MinitouchAgent extends Thread {
    private static final String TAG = MinitouchAgent.class.getSimpleName();
    private static final String SOCKET = "minitouchagent";
    private static final int DEFAULT_MAX_CONTACTS = 10;
    private static final int DEFAULT_MAX_PRESSURE = 0;
    private int rotation;
    private int width;
    private int height;
    private LocalServerSocket serverSocket = null;
    private LocalSocket clientSocket = null;
    private BufferedReader reader = null;

    private MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[2];
    private MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2];
    private PointerEvent[] events = new PointerEvent[2];

    private final InputManagerWrapper inputManager;
    private final WindowManagerWrapper windowManager;
    private final Handler handler;

    private class PointerEvent {
        long lastMouseDown;
        int lastX;
        int lastY;
        int action;
    }

    private void injectEvent(final InputEvent event) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                inputManager.injectInputEvent(event);
            }
        });
    }

    private MotionEvent getMotionEvent(PointerEvent p) {
        return getMotionEvent(p, 0);
    }

    private void pointer2coords(PointerEvent p, MotionEvent.PointerCoords coords) {
        switch(rotation) {
            case 0:
                coords.x = p.lastX;
                coords.y = p.lastY;
                break;
            case 1:
                coords.x = p.lastY;
                coords.y = width - p.lastX;
                break;
            case 2:
                coords.x = width - p.lastX;
                coords.y = height - p.lastY;
                break;
            default:
                coords.x = height - p.lastY;
                coords.y = p.lastX;
                break;
        }

        String event;
        switch(p.action) {
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_2_UP:
                event = "ACTION_UP";
                break;
            case MotionEvent.ACTION_MOVE:
                event = "ACTION_MOVE";
                break;
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_2_DOWN:
                event = "ACTION_DOWN";
                break;
            default:
                event = "NONE";
                break;
        }
        
        Log.v(TAG, "Event: " + event + ", Pointer: " + p.lastX + "," + p.lastY + ", Coords: " + coords.x + "," + coords.y);
    }

    private MotionEvent getMotionEvent(PointerEvent p, int idx) {
        long now = SystemClock.uptimeMillis();
        if (p.action == MotionEvent.ACTION_DOWN) {
            p.lastMouseDown = now;
        }
        pointer2coords(p, pointerCoords[idx]);
        return MotionEvent.obtain(p.lastMouseDown, now, p.action, idx + 1, pointerProperties,
                pointerCoords, 0, 0, 1f, 1f, 0, 0,
                InputDevice.SOURCE_TOUCHSCREEN, 0);
    }

    private List<MotionEvent> getMotionEvent(PointerEvent p1, PointerEvent p2) {
        List<MotionEvent> combinedEvents = new ArrayList<>(2);
        long now = SystemClock.uptimeMillis();
        if (p1.action != MotionEvent.ACTION_MOVE) {
            combinedEvents.add(getMotionEvent(p1));
            combinedEvents.add(getMotionEvent(p2, 1));
        } else {
            pointer2coords(p1, pointerCoords[0]);
            pointer2coords(p2, pointerCoords[1]);

            MotionEvent event = MotionEvent.obtain(p1.lastMouseDown, now, p1.action, 2, pointerProperties,
                    pointerCoords, 0, 0, 1f, 1f, 0, 0,
                    InputDevice.SOURCE_TOUCHSCREEN, 0);
            combinedEvents.add(event);
        }
        return combinedEvents;
    }

    private static String getPid() throws IOException {
        byte[] bo = new byte[256];
        InputStream is = new FileInputStream("/proc/self/stat");
        is.read(bo);
        for (int i = 0; i < bo.length; i++) {
            if ((bo[i] < '0') || (bo[i] > '9')) {
                return new String(bo, 0, i);
            }
        }
        return "-1";
    }

    /**
     * v 1
     * ^ 10 1080 2340 0
     * $ 1078
     */
    private void sendBanner() {
        try {
            OutputStreamWriter out = new OutputStreamWriter(clientSocket.getOutputStream());
            out.write("v 1\n");
            String resolution = String.format(Locale.US, "^ %d %d %d %d%n",
                    DEFAULT_MAX_CONTACTS, width, height, DEFAULT_MAX_PRESSURE);
            out.write(resolution);
            out.write(String.format(Locale.US, "$ %s%n", getPid()));
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Manages the client connection. The client is supposed to be minitouch.
     */
    private void manageClientConnection() {
        Log.v(TAG, "Listening on localabstract:" + SOCKET);

        while (true) {
            try {
                clientSocket = serverSocket.accept();
                Log.d(TAG, "client connected");
                sendBanner();
                processCommandLoop();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if(clientSocket != null) {
                    try {
                        clientSocket.close();
                    } catch(Exception e) {
                        Log.d(TAG, "close client socket", e);
                    }
                    clientSocket = null;
                }
            }
        }
    }

    /**
     * processCommandLoop parses touch related commands sent by stf
     * and inject them in Android InputManager.
     * Commmands can be of type down, up, move, commit
     * Note that it currently doesn't support multitouch
     */
    private void processCommandLoop() throws IOException {
        String cmd;
        int count = 0;
        reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

        try {
            while ((cmd = reader.readLine()) != null) {
                try (Scanner scanner = new Scanner(cmd)) {
                    scanner.useDelimiter(" ");
                    String type = scanner.next();
                    int contact;
                    switch (type) {
                        case "c":
                            if (count == 1) {
                                MotionEvent event = getMotionEvent(events[0]);
                                injectEvent(event);
                            } else if (count == 2) {
                                for (MotionEvent event : getMotionEvent(events[0], events[1])) {
                                    injectEvent(event);
                                }
                            } else {
                                Log.v(TAG, "count not manage events #" + count);
                            }
                            count = 0;
                            break;
                        case "u":
                            count++;
                            contact = scanner.nextInt();
                            events[contact].action = (contact == 0) ? MotionEvent.ACTION_UP : MotionEvent.ACTION_POINTER_2_UP;
                            break;
                        case "d":
                            count++;
                            contact = scanner.nextInt();
                            events[contact].lastX = scanner.nextInt();
                            events[contact].lastY = scanner.nextInt();
                            //scanner.nextInt(); //pressure is currently not supported
                            events[contact].action = (contact == 0) ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_POINTER_2_DOWN;
                            break;
                        case "m":
                            count++;
                            contact = scanner.nextInt();
                            events[contact].lastX = scanner.nextInt();
                            events[contact].lastY = scanner.nextInt();
                            //scanner.nextInt(); //pressure is currently not supported
                            events[contact].action = MotionEvent.ACTION_MOVE;
                            break;
                        case "w":
                            int delayMs = scanner.nextInt();
                            Thread.sleep(delayMs);
                            break;
                        default:
                            Log.v(TAG, "could not parse: " + cmd);
                    }
                } catch (NoSuchElementException e) {
                    Log.v(TAG, "could not parse: " + cmd);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            if(reader != null) {
                reader.close();
                reader = null;
            }
        }
    }

    public MinitouchAgent(Handler handler) {
        this.handler = handler;
        inputManager = new InputManagerWrapper();
        windowManager = new WindowManagerWrapper();
        MotionEvent.PointerProperties pointerProps0 = new MotionEvent.PointerProperties();
        pointerProps0.id = 0;
        pointerProps0.toolType = MotionEvent.TOOL_TYPE_FINGER;
        MotionEvent.PointerProperties pointerProps1 = new MotionEvent.PointerProperties();
        pointerProps1.id = 1;
        pointerProps1.toolType = MotionEvent.TOOL_TYPE_FINGER;
        pointerProperties[0] = pointerProps0;
        pointerProperties[1] = pointerProps1;

        MotionEvent.PointerCoords pointerCoords0 = new MotionEvent.PointerCoords();
        MotionEvent.PointerCoords pointerCoords1 = new MotionEvent.PointerCoords();
        pointerCoords0.orientation = 0;
        pointerCoords0.pressure = 1; // pressure and size have to be set
        pointerCoords0.size = 1;
        pointerCoords1.orientation = 0;
        pointerCoords1.pressure = 1;
        pointerCoords1.size = 1;
        pointerCoords[0] = pointerCoords0;
        pointerCoords[1] = pointerCoords1;

        events[0] = new PointerEvent();
        events[1] = new PointerEvent();

        rotation = windowManager.getRotation();
        Point p = windowManager.getDisplaySize();
        width = p.x;
        height = p.y;
//        if(rotation % 2 == 0) {
//            width = p.x;
//            height = p.y;
//        } else {
//            width = p.y;
//            height = p.x;
//        }

        windowManager.watchRotation(r -> {
            rotation = r;

//            Point p2 = windowManager.getDisplaySize();
//            if(rotation % 2 == 0) {
//                width = p2.x;
//                height = p2.y;
//            } else {
//                width = p2.y;
//                height = p2.x;
//            }
            Log.v(TAG, "Rotation: " + r + ", width: " + width + ", height: " + height);

//            if(clientSocket != null) {
//                try {
//                    clientSocket.shutdownInput();
//                    clientSocket.shutdownOutput();
//                    clientSocket.close();
//                    Log.v(TAG, "Closed connect");
//                } catch(Exception e) {
//                    Log.e(TAG, "Close connect error", e);
//                }
//                clientSocket = null;
//            }
//
//            if(reader != null) {
//                try {
//                    reader.close();
//                    Log.v(TAG, "Closed reader");
//                } catch(Exception e) {
//                    Log.e(TAG, "Close reader error", e);
//                }
//                reader = null;
//            }
        });
    }

    @Override
    public void run() {
        try {
            Log.i(TAG, String.format("creating socket %s", SOCKET));
            serverSocket = new LocalServerSocket(SOCKET);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        manageClientConnection();
        try {
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
